﻿//--------------------------------------------------------------------------------------------------------------------------------
// <copyright file="LinearTwoLayersScheme.cs" company="Exul">
//     Copyright (c) Exul. All rights reserved.
// </copyright>
// <author>Exul</author>
//--------------------------------------------------------------------------------------------------------------------------------

namespace ExulLibrary.FiniteElementMethod
{
    using System;
    using System.Collections.Generic;

    /// <summary>Represents a linear two layers interpolation scheme.</summary>
    public sealed class LinearTwoLayersScheme : Scheme
    {
        /// <summary>The index of the current layer.</summary>
        private int currentLayerIndex;

        /// <summary>The value for the current layer.</summary>
        private double layerZero;

        /// <summary>The value for the previous layer.</summary>
        private double layerOne;

        /// <summary>A value indicating whether the additional values is calculated.</summary>
        private bool isSubtractionsCalculated;

        /// <summary>The length of the interval between the previous layer and the current layer.</summary>
        private double lengthZeroOne;

        /// <summary>Initializes a new instance of the <see cref="LinearTwoLayersScheme"/> class.</summary>
        /// <param name="layers">The layers values.</param>
        /// <exception cref="ArgumentNullException"><paramref name="layers"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException"><paramref name="layers"/> is empty collection.</exception>
        public LinearTwoLayersScheme(IList<double> layers) : base(layers)
        {
            this.Reset();
        }

        /// <summary>Gets the functions count that is used by the scheme.</summary>
        /// <value>The functions count that is used by the scheme.</value>
        public override int FunctionsCount
        {
            get
            {
                return 2;
            }
        }

        /// <summary>Gets the value of first layer.</summary>
        /// <value>The value of first layer.</value>
        public override double FirstLayerValue
        {
            get
            {
                return this.layerZero;
            }
        }

        /// <summary>Gets the value of last layer.</summary>
        /// <value>The value of last layer.</value>
        public override double LastLayerValue
        {
            get
            {
                return this.layerOne;
            }
        }

        /// <summary>Determines whether a function with the specified index is defined at the current state.</summary>
        /// <param name="functionIndex">The index of the function.</param>
        /// <returns><see langword="true"/> if the function is defined; otherwise, <see langword="false"/>.</returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     <paramref name="functionIndex"/> is negative or greater than or equal to the functions count.
        /// </exception>
        public override bool IsFunctionDefined(int functionIndex)
        {
            switch (functionIndex)
            {
                case 0:
                    return this.currentLayerIndex != 0;
                case 1:
                    return this.currentLayerIndex != 0;
                default:
                    return false;
            }
        }

        /// <summary>Calculates a function with the specified index in a specified point.</summary>
        /// <param name="point">The point to calculate.</param>
        /// <param name="functionIndex">The index of the function to calculate.</param>
        /// <returns>
        ///     The value of the function with the specified index in the specified point or
        ///     zero if <paramref name="point"/> is not between <see cref="Scheme.FirstLayerValue"/> and
        ///     <see cref="Scheme.LastLayerValue"/>.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     <paramref name="functionIndex"/> is negative or greater than or equal to the functions count.
        /// </exception>
        public override double CalculateFunction(double point, int functionIndex)
        {
            #if SAFE

            if (this.currentLayerIndex != 0)
            {
                throw ThrowHelper.SchemeFunctionIsNotDefined(functionIndex, this.currentLayerIndex);
            }

            if ((functionIndex < 0) || (1 < functionIndex))
            {
                throw ThrowHelper.FunctionIndexIsOutOfRange("functionIndex", functionIndex, 0, 1);
            }

            #endif

            // if the point is outside of the definition area);
            if ((point < this.layerOne) || (this.layerZero < point))
            {
                return 0.0;
            }

            if (!this.isSubtractionsCalculated)
            {
                this.CalculateSubtractions();
            }

            return functionIndex == 0
                       ? (point - this.layerOne) / this.lengthZeroOne
                       : (this.layerZero - point) / this.lengthZeroOne;
        }

        /// <summary>Calculates derivative of a function with the specified index in the specified point.</summary>
        /// <param name="point">The point to calculate.</param>
        /// <param name="functionIndex">The index of the function to calculate.</param>
        /// <returns>
        ///     The value of the function derivative with the specified index in the specified point or
        ///     zero if <paramref name="point"/> is not between <see cref="Scheme.FirstLayerValue"/> and
        ///     <see cref="Scheme.LastLayerValue"/>.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     <paramref name="functionIndex"/> is negative or greater than or equal to the functions count.
        /// </exception>
        public override double CalculateDerivative(double point, int functionIndex)
        {
            #if SAFE

            if (this.currentLayerIndex != 0)
            {
                throw ThrowHelper.SchemeFunctionIsNotDefined(functionIndex, this.currentLayerIndex);
            }

            if ((functionIndex < 0) || (1 < functionIndex))
            {
                throw ThrowHelper.FunctionIndexIsOutOfRange("functionIndex", functionIndex, 0, 1);
            }

            #endif

            // if the point is outside of the definition area
            if ((point < this.layerOne) || (this.layerZero < point))
            {
                return 0.0;
            }

            if (!this.isSubtractionsCalculated)
            {
                this.CalculateSubtractions();
            }

            return functionIndex == 0
                       ? 1.0 / this.lengthZeroOne
                       : -1.0 / this.lengthZeroOne;
        }

        /// <summary>Calculates derivative of a function with the specified index on the first layer.</summary>
        /// <param name="functionIndex">The index of the function to calculate.</param>
        /// <returns>The value of the function derivative with the specified index in the specified point.</returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     <paramref name="functionIndex"/> is negative or greater than or equal to the functions count.
        /// </exception>
        public override double CalculateDerivativeOnFirstLayer(int functionIndex)
        {
            #if SAFE

            if (this.currentLayerIndex != 0)
            {
                throw ThrowHelper.SchemeFunctionIsNotDefined(functionIndex, this.currentLayerIndex);
            }

            if ((functionIndex < 0) || (1 < functionIndex))
            {
                throw ThrowHelper.FunctionIndexIsOutOfRange("functionIndex", functionIndex, 0, 1);
            }

            #endif

            if (!this.isSubtractionsCalculated)
            {
                this.CalculateSubtractions();
            }

            return functionIndex == 0
                ? 1.0 / this.lengthZeroOne
                : -1.0 / this.lengthZeroOne;
        }

        /// <summary>Moves the current state to a next layer.</summary>
        /// <returns><see langword="true"/> if the movement is successful; otherwise, <see langword="false"/>.</returns>
        public override bool MoveToNextLayer()
        {
            if (this.currentLayerIndex == this.Layers.Count - 1)
            {
                return false;
            }

            this.currentLayerIndex++;
            this.layerOne = this.layerZero;
            this.layerZero = this.Layers[this.currentLayerIndex];
            this.isSubtractionsCalculated = false;
            return true;
        }

        /// <summary>Sets the current state to the initial position.</summary>
        public override void Reset()
        {
            this.isSubtractionsCalculated = false;
            this.currentLayerIndex = 0;
            this.layerOne = this.layerZero = this.Layers[this.currentLayerIndex];
        }

        /// <summary>Calculates the additional values that are required to calculate the functions.</summary>
        private void CalculateSubtractions()
        {
            this.lengthZeroOne = this.layerZero - this.layerOne;
            this.isSubtractionsCalculated = true;
        }
    }
}